﻿using System;
using System.Text;

namespace Ndns.Packet.Text
{
    internal class PunycodeDecoder : Decoder
    {
        private static PunycodeDecoder _instance = new PunycodeDecoder();
        public static PunycodeDecoder Default => _instance;

        public override int GetCharCount(byte[] bytes, int index, int count)
        {
            int n = Punycode.INITIAL_N;
            int i = 0;
            int bias = Punycode.INITIAL_BIAS;
            int len = 0;
            int d = RelativeLastIndexOf(bytes, index, count, (byte)Punycode.DELIMITER);

            if (d > 0)
            {
                for (int j = 0; j < d; j++)
                {
                    char c = (char)bytes[index + j];
                    if (!Punycode.IsBasic(c))
                    {
                        throw new ArgumentException();
                    }
                    len++;
                }
                d++;
            }
            else
            {
                d = 0;
            }

            while (d < count)
            {
                int oldi = i;
                int w = 1;
                for (int k = Punycode.BASE; ; k += Punycode.BASE)
                {
                    if (d == count)
                    {
                        throw new ArgumentException();
                    }
                    int c = bytes[index + d++];
                    int digit = Punycode.CodepointToDigit(c);
                    if (digit > (int.MaxValue - i) / w)
                    {
                        throw new OverflowException();
                    }
                    i = i + digit * w;
                    int t;
                    if (k <= bias)
                    {
                        t = Punycode.TMIN;
                    }
                    else if (k >= bias + Punycode.TMAX)
                    {
                        t = Punycode.TMAX;
                    }
                    else
                    {
                        t = k - bias;
                    }
                    if (digit < t)
                    {
                        break;
                    }
                    w = w * (Punycode.BASE - t);
                }
                bias = Punycode.Adapt(i - oldi, len + 1, oldi == 0);
                if (i / (len + 1) > int.MaxValue - n)
                {
                    throw new OverflowException();
                }
                n = n + i / (len + 1);
                i = i % (len + 1);
                len++;
                i++;
            }
            return len;
        }

        public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
        {
            int n = Punycode.INITIAL_N;
            int i = 0;
            int bias = Punycode.INITIAL_BIAS;
            int len = 0;
            int d = RelativeLastIndexOf(bytes, byteIndex, byteCount, (byte)Punycode.DELIMITER);

            if (d > 0)
            {
                for (int j = 0; j < d; j++)
                {
                    char c = (char)bytes[byteIndex + j];
                    if (!Punycode.IsBasic(c))
                    {
                        throw new ArgumentException();
                    }
                    chars[charIndex + len++] = c;
                }
                d++;
            }
            else
            {
                d = 0;
            }

            while (d < byteCount)
            {
                int oldi = i;
                int w = 1;
                for (int k = Punycode.BASE; ; k += Punycode.BASE)
                {
                    if (d == byteCount)
                    {
                        throw new ArgumentException();
                    }
                    int c = bytes[byteIndex + d++];
                    int digit = Punycode.CodepointToDigit(c);
                    if (digit > (int.MaxValue - i) / w)
                    {
                        throw new OverflowException();
                    }
                    i = i + digit * w;
                    int t;
                    if (k <= bias)
                    {
                        t = Punycode.TMIN;
                    }
                    else if (k >= bias + Punycode.TMAX)
                    {
                        t = Punycode.TMAX;
                    }
                    else
                    {
                        t = k - bias;
                    }
                    if (digit < t)
                    {
                        break;
                    }
                    w = w * (Punycode.BASE - t);
                }
                bias = Punycode.Adapt(i - oldi, len + 1, oldi == 0);
                if (i / (len + 1) > int.MaxValue - n)
                {
                    throw new OverflowException();
                }
                n = n + i / (len + 1);
                i = i % (len + 1);
                ArrayInsert(chars, charIndex + i, len - i, (char)n); // output.Insert(i, (char)n);
                len++;
                i++;
            }
            return len;
        }

        private static int RelativeLastIndexOf(byte[] array, int startIndex, int count, byte element)
        {
            for (int pos = startIndex + count - 1; pos >= startIndex; pos--)
            {
                if (array[pos] == element)
                    return pos - startIndex;
            }
            return -1;
        }
        
        private static void ArrayRightShift(char[] array, int startIndex, int count, int shiftLength)
        {
            for (int pos = startIndex + count - 1; pos >= startIndex; pos--)
            {
                array[pos + shiftLength] = array[pos];
            }
        }

        private static void ArrayInsert(char[] array, int index, int rightLength, char element)
        {
            ArrayRightShift(array, index, rightLength, 1);
            array[index] = element;
        }
    }
}
